package com.example.function.integration.utils;

import com.example.function.integration.sftp.CrlSftpVo;
import com.jcraft.jsch.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Vector;

/**
 * @description:SFTP工具类
 * @className:SftpUtil
 * @author:ext.fengcheng
 * @date:2023/9/27 15:33
 */
public class SftpUtil {
    /**
     * 链接时间同一个(单位毫秒)
     */
    private final int connectTimeout = 18000 * 1000;
    /**
     * SFTP-通道对象
     */
    private ChannelSftp sftp;
    /**
     * SFTP-ip地址
     */
    private final String host;
    /**
     * SFTP-端口
     */
    private final int port;
    /**
     * SFTP-登录账号
     */
    private final String username;
    /**
     * SFTP-登录密码
     */
    private String password;
    /**
     * SFTP-登录秘钥文件
     */
    private String privateKeyFile;
    /**
     * SFTP-会话对象
     */
    private Session sshSession;
    private static final Logger logger = LoggerFactory.getLogger(SftpUtil.class);

    /**
     * 构造函数，根据私钥产生对象
     *
     * @param username
     * @param privateKeyFile
     * @param host
     * @param port
     * @return
     * @throws Exception
     */
    public SftpUtil(String username, String privateKeyFile, String host, int port) throws JSchException {
        this.privateKeyFile = privateKeyFile;
        this.host = host;
        this.username = username;
        this.port = port;
        getConnectByPrivateKey();
    }

    /**
     * 构造函数，根据用户名密码产生对象
     *
     * @param username
     * @param password
     * @param host
     * @param port
     * @param sign     没有用到可以传入""值，是为了与私钥生产对象的构造函数区分
     * @return
     * @throws Exception
     */
    public SftpUtil(String host, String username, String password, int port, String sign) throws JSchException {
        this.host = host;
        this.username = username;
        this.password = password;
        this.port = port;
        //获取链接
        getConnectByPassword();
    }

    /**
     * 根据配置构造连接对象, 如果配置了密码优先使用密码,如果没有密码再使用私钥
     * 私钥文件存放在类加载根路径,私钥文件名称为: privateKeyFile+username
     *
     * @param sftpVo
     * @return
     */
    public static SftpUtil buildSftpUtil(CrlSftpVo sftpVo) throws JSchException {
        if (sftpVo == null) {
            throw new NullPointerException("构造sftp连接对象失败,未进行相关配置.");
        }
        if (StringUtils.isBlank(sftpVo.getHost()) ||
                sftpVo.getPort() == 0 ||
                StringUtils.isBlank(sftpVo.getUsername())) {
            throw new NullPointerException("构造sftp连接对象失败,必填参数不能为空.");
        }

        SftpUtil util = null;
        if (StringUtils.isNotBlank(sftpVo.getPassword())) {
            //密码形式构造
            util = new SftpUtil(sftpVo.getHost(), sftpVo.getUsername(), sftpVo.getPassword(), sftpVo.getPort(), "");
        } else if (StringUtils.isNotBlank(sftpVo.getPrivateKeyFile())) {
            //私钥形式构造
            URL url = SftpUtil.class.getClassLoader().getResource(sftpVo.getPrivateKeyFile());
            if (url == null) {
                throw new NullPointerException("获取私钥文件失败,请检查是否在类路径下配置私钥文件");
            }
            String authKey = url.getPath();
            util = new SftpUtil(sftpVo.getUsername(), authKey, sftpVo.getHost(), sftpVo.getPort());
        } else {
            throw new RuntimeException("构造sftp连接对象失败,密码和私钥不能同时为空.");
        }
        return util;
    }

    /**
     * 账户密码连接方式
     */
    private void getConnectByPassword() throws JSchException {
        logger.info("连接SFTP（账户&密码）开始|username:{}|this.host:{}|this.port:{}", this.username, this.host, this.port);
        JSch e = new JSch();
        e.getSession(this.username, this.host, this.port);
        this.sshSession = e.getSession(this.username, this.host, this.port);
        this.sshSession.setPassword(this.password);
        Properties sshConfig = new Properties();
        sshConfig.put("StrictHostKeyChecking", "no");
        this.sshSession.setConfig(sshConfig);
        this.sshSession.connect(connectTimeout);
        this.sshSession.setTimeout(connectTimeout);
        Channel channel = this.sshSession.openChannel("sftp");
        channel.connect(connectTimeout);
        this.sftp = (ChannelSftp) channel;
    }

    /**
     * 根据私钥获取连接
     *
     * @return
     * @throws JSchException
     */
    private void getConnectByPrivateKey() throws JSchException {
        logger.info("连接SFTP（账户&私钥）开始|username:{}|this.host:{}|this.port:{}", this.username, this.host, this.port);
        JSch jsch = new JSch();
        jsch.addIdentity(privateKeyFile);
        this.sshSession = jsch.getSession(username, host, port);
        sshSession.setConfig("StrictHostKeyChecking", "no");
        this.sshSession.connect(connectTimeout);
//        this.sshSession.setTimeout(timeOut);
        Channel channel = this.sshSession.openChannel("sftp");
        channel.connect(connectTimeout);
        this.sftp = (ChannelSftp) channel;
    }

    /**
     * 关闭连接
     */
    public void disconnect() {
        if (this.sshSession != null && this.sshSession.isConnected()) {
            this.sshSession.disconnect();
        }

        if (this.sftp != null && this.sftp.isConnected()) {
            this.sftp.disconnect();
        }
    }

    /**
     * 下载远程文件到本地流
     *
     * @param remotePath
     * @param fileName
     * @return
     * @throws Exception
     */
    public InputStream downFile(String remotePath, String fileName) throws Exception {
        logger.info("SftpUtil|downFile|下载远程文件到本地流|remotePath:{}|fileName:{}", remotePath, fileName);

        remotePath = getAbsolutePath(remotePath);
        if (!existFile(remotePath, fileName)) {
            logger.info("SftpUtil|downFile|下载远程文件到本地流|文件不存在|remotePath:{}|fileName:{}", remotePath, fileName);
            return null;
        }

        this.sftp.cd(remotePath);
        return this.sftp.get(fileName);
    }

    /**
     * @Description：从SFTP下载文件到本地
     * @Author： ext.fengcheng
     * @Date：2023/9/27 16:47
     * @remotePath： SFTP文件目录
     * @remoteFileName： SFTP文件目录下的文件名
     * @localPath： 本地文件目录
     * @localFileName： 本地文件目录下存放文件名
     * @Return： boolean
     */
    public boolean downloadFile(String remotePath, String remoteFileName, String localPath, String localFileName) throws IOException {
        OutputStream out = null;
        try {
            remotePath = getAbsolutePath(remotePath);
            localPath = getAbsolutePath(localPath);
            logger.info("SftpUtil|downloadFile|下载远程文件到本地开始|remotePath:{}|remoteFileName:{}|localPath:{}|localFileName:{}", remotePath, remoteFileName, localPath, localFileName);
            this.sftp.cd(remotePath);
            File e = new File(localPath + localFileName);
            boolean exists = mkdirs(localPath + localFileName);
            if (exists) {
                logger.info("SftpUtil|downloadFile|下载远程文件到本地|创建本地文件完成|localPath:{}|localFileName:{}", localPath, localFileName);
            } else {
                logger.warn("SftpUtil|downloadFile|下载远程文件到本地|创建本地文件失败|localPath:{}|localFileName:{}", localPath, localFileName);
            }
            out = Files.newOutputStream(e.toPath());
            this.sftp.get(remoteFileName, out);
            return true;
        } catch (Exception e) {
            logger.error("SftpUtil|downloadFile|下载远程文件到本地异常|remotePath:{}|remoteFileName:{}|localPath:{}|localFileName:{}", remotePath, remoteFileName, localPath, localFileName, e);
        } finally {
            if (null != out) {
                out.close();
            }
        }
        return false;
    }

    /**
     * 批量下载文件
     * //TODO 该方法每下一个文件都建立一个连接, 效率较低, 后续需要重新实现
     *
     * @param remotePath：远程下载目录(以路径符号结束,可以为相对路径eg:/assess/sftp/jiesuan_2/2014/)
     * @param localPath：本地保存目录(以路径符号结束,D:\Duansha\sftp\)
     * @return
     */
    public List<String> batchDownLoadFile(String remotePath, String localPath) {
        List<String> filenames = new ArrayList<>();
        String methodDesc = "批量下载文件";
        try {
            logger.info(methodDesc + "开始" + "remotePath:" + remotePath + "localPath=" + localPath);
            logger.info("批量下载文件|开始|remotePath:{}|localPath:{}", remotePath, localPath);
            Vector v = listFiles(remotePath);
            if (!v.isEmpty()) {
                for (Object o : v) {
                    if (o instanceof ChannelSftp.LsEntry) {
                        ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) o;
                        String filename = entry.getFilename();
                        SftpATTRS attrs = entry.getAttrs();
                        if (!attrs.isDir()) {
                            String localFileName = localPath + filename;
                            boolean flag = downloadFile(remotePath, filename, localPath, filename);
                            if (flag) {
                                filenames.add(localFileName);
                            }
                        }
                    } else {
                        logger.warn("类型错误");
                    }
                }
            }
            logger.info("批量下载文件|成功|remotePath:{}|localPath:{}|文件总数:{}", remotePath, localPath, v.size());
        } catch (Exception e) {
            logger.error("批量下载文件|成功|remotePath:{}|localPath:{}", remotePath, localPath, e);
        }
        return filenames;
    }

    /**
     * 上传文件
     *
     * @param input      文件输入流
     * @param fileName   文件名称
     * @param remotePath SFTP文件存放目录
     * @throws Exception
     */
    public void uploadFile(InputStream input, String fileName, String remotePath) throws Exception {
        try {
            // 判断远程文件夹是否存在,不存在，则进行创建
            createRemoteDir(remotePath);
            // 上传文件操作
            this.sftp.put(input, fileName);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (Exception e) {
                    logger.error("上传失败", e);
                }
            }
        }
    }

    /**
     * 目录下文件是否存在
     *
     * @param remotePath
     * @param fileName
     * @return
     */
    public boolean existFile(String remotePath, String fileName) throws IOException {
        InputStream in = null;
        try {
            remotePath = getAbsolutePath(remotePath);
            if (!existDir(remotePath)) {
                return false;
            }

            String fullName = remotePath + fileName;
            in = this.sftp.get(fullName);
            logger.info("existFile|判断文件是否存在|文件已存在，不需要上传|remotePath:{}|fileName:{}|result:{}", remotePath, fileName, in != null);
            return in != null;
        } catch (SftpException e) {
            logger.info("existFile|判断文件是否存在|文件不存在，需要上传|remotePath:{}|fileName:{}", remotePath, fileName);
            return false;
        } catch (Exception e) {
            logger.error("existFile|判断文件是否存在发生异常|remotePath:{}|fileName:{}", remotePath, fileName, e);
            return false;
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    public static boolean existStaticFile(SftpUtil sftpUtil, String remotePath, String fileName) {
        InputStream in = null;
        try {
            remotePath = getStaticAbsolutePath(remotePath);
            if (!existStaticDir(remotePath, sftpUtil)) {
                return false;
            }
            String fullName = remotePath + fileName;
            in = sftpUtil.sftp.get(fullName);
            boolean b = in != null;
            if (in != null) {
                in.close();
            }
            return b;
        } catch (SftpException e) {
            logger.info("existStaticFile|文件不存在，需要上传|remotePath:{}", remotePath);
            return false;
        } catch (Exception e) {
            logger.error("existFile|判断文件是否存在发生异常|remotePath:{}|fileName:{}", remotePath, fileName, e);
            return false;
        }
    }

    /**
     * 获取文件绝对路径
     *
     * @param path
     * @return
     */
    private String getAbsolutePath(String path) {
        if (!path.endsWith("/")) {
            path = path + "/";
        }
        return path;
    }

    /**
     * 获取文件绝对路径
     *
     * @param path
     * @return
     */
    public static String getStaticAbsolutePath(String path) {
        if (!path.endsWith("/")) {
            path = path + "/";
        }
        return path;
    }

    /**
     * 注意这里采用绝对路径
     *
     * @param remotePath
     * @throws SftpException
     * @throws JSchException
     */
    public void createRemoteDir(String remotePath) {
        try {
            if (this.existDir(remotePath)) {
                this.sftp.cd(remotePath);
                return;
            }
            this.sftp.mkdir(remotePath);
            this.sftp.cd(remotePath);
        } catch (SftpException e) {
            logger.error("判断远程目录是否存在,发生异常，远程目录：{}, 异常信息：{}", remotePath, e.getMessage(), e);
        }
    }

    /**
     * 判断远程目录是否存在
     *
     * @param remotePath
     * @return
     * @throws SftpException
     * @throws JSchException
     */
    public boolean existDir(String remotePath) {
        try {
            SftpATTRS attrs = this.sftp.lstat(remotePath);
            return attrs.isDir();
        } catch (Exception e) {
            logger.warn("当前远程目录不存在，remotePath：【" + remotePath + "】");
            return false;
        }
    }

    public static boolean existStaticDir(String remotePath, SftpUtil sftpUtil) {
        try {
            SftpATTRS attrs = sftpUtil.sftp.lstat(remotePath);
            return attrs.isDir();
        } catch (Exception e) {
            logger.warn("当前远程目录不存在，remotePath：【" + remotePath + "】");
            return false;
        }
    }

    /**
     * 删除文件
     *
     * @param directory
     * @param deleteFile
     * @return
     * @throws SftpException
     */
    public boolean deleteSFTP(String directory, String deleteFile) throws SftpException {
        logger.info("SftpUtil|deleteSFTP|删除文件开始|文件夹: {} | 文件名:{}", directory, deleteFile);
        this.sftp.cd(directory);
        this.sftp.rm(deleteFile);
        return true;
    }

    /**
     * 删除文件目录
     *
     * @param directory
     * @throws SftpException
     */
    public void deleteSFTPDirectory(String directory) throws SftpException {
        logger.info("SftpUtil|deleteSFTP|删除文件夹目录开始|文件目录:{}", directory);
        this.sftp.rmdir(directory + "*");
    }

    /**
     * 创建父文件夹
     *
     * @param path
     * @return
     */
    public static boolean mkdirs(String path) {
        logger.info("SftpUtil|mkdirs|创建文件开始|path: {}", path);
        File file = new File(path);
        File parent = file.getParentFile();
        if (!parent.exists()) {
            parent.mkdirs();
        }
        return parent.exists();
    }

    /**
     * 获取文件真正的绝对路径
     *
     * @param path
     * @return
     */
    public String getAbsoluteTruePath(String path) throws SftpException {
        if (!path.endsWith("/")) {
            path = path + "/";
        }
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        path = this.sftp.pwd() + path;
        return path;
    }

    /**
     * 列出目录下的文件
     *
     * @param directory：要列出的目录
     * @return
     * @throws SftpException
     */
    public Vector listFiles(String directory) {
        try {
            return sftp.ls(directory);
        } catch (SftpException e) {
            logger.error("listFiles.文件目录【" + directory + "】不存在，统计失败");
            return null;
        }
    }
}
